"""
Module to support slave data bases for the Passive Replication model.

Slaves just listen for data and update their internal state. They can
turn into Coordinator if desired.

Last Modified: $Id: slave.py 44 2005-06-15 00:31:25Z dafny $

@see: L{db.DB}

@author: U{Gustavo Sverzut Barbieri <barbieri@gmail.com>}
@license: GNU GPL
@copyright: (C) 2005 Gustavo Sverzut Barbieri
@version: $Rev: 44 $
"""

import socket
import commands
import rawdb
import sys
import time
import tempfile
import os
from commandserver import command_recv, command_send
from debug import debug, format_exc

class Slave( object ):
    """A slave client.

    This knows how to communicate with
    L{SlaveRequestHandler<coordinator.Coordinator.SlaveServer.SlaveRequestHandler>} and thus
    Coordinators, get and send data and eventually become a
    coordinator.

    @todo: turn a slave into coordinator. Maybe in another class?
    @todo: receive request id + responde and send ack.
    """
    timeout = 5.0

    def __init__( self, coordinator_address ):
        """Creates a new Slave that connects to coordinator.

        @param coordinator_address: a tuple as required by
        L{socket.connect<socket.socket.connect>}.
        """
	self.__db       = rawdb.RawDB()
        self.__journal  = []
        self.__requests = {}
        self.__req_ver  = {}
	self.connect_to( coordinator_address )
        self.server_address = coordinator_address

        self.introduced_delay = None
    # __init__()


    def create_coordinator( self, slave_server_address, slave_timeout=None ):
        """Create coordinator from this slave.

        This can be used to convert a Slave into a Coordinator.

        @param slave_server_address: where to wait for slaves, must be a tuple
                                     C{( address, port )}, ie
                                     C{( '', 8000 )}.
        @type  slave_server_address: L{tuple}

        @param slave_timeout: how many seconds before issuing timeout on
                              network operations.
        @type  slave_timeout: L{float}

        @see: L{Coordinator}
        """
        from coordinator import Coordinator
        from bank import VersionedBank
        from db import VersionedDB
        c = Coordinator( slave_server_address, slave_timeout,
                         VersionedBank( VersionedDB( self.__db,
                                                     self.__journal ) ),
                         self.__requests,
                         self.__req_ver )
        return c
    # create_coordinator()


    def connect_to( self, coordinator_address ):
        """Connect to the coordinator.

        @param coordinator_address: a tuple as required by
        L{socket.connect<socket.socket.connect>}.
        """
	self.__sock = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
        self.__sock.setblocking( 1 )
        self.__sock.settimeout( self.timeout )
	self.__sock.connect( coordinator_address )
    # connect_to()


    def recv( self ):
        """Receive command and its data from coordinator.

        @return: a tuple C{(command, data)}.

        @see: L{commandserver.command_recv}
        """
        return command_recv( self.__sock.recv )
    # recv()


    def send( self, command, data ):
        """Send command and its data to coordinator.

        @param command: any value from L{commands}.
        @param data: any python object to send as data.

        @see: L{commandserver.command_send}
        """
        return command_send( self.__sock.send, command, data )
    # send()


    def send_ack( self ):
        self._debug( 2, "Send ACK" )
        self.send( commands.ACK, None )
    # send_ack()


    def recv_ack( self ):
        self._debug( 2, "Wait for ACK" )
        cmd, data = self.recv()
        if cmd == commands.ACK:
            self._debug( 2, "Got ACK" )
            return True
        else:
            self._debug( 0, "could not receive ACK, got %s instead." % \
                         ( cmd, ) )
            return False
    # recv_ack()


    def stop( self ):
        self.keep_running = False
    # stop()


    def run( self, retrieve_db=True ):
        """Start interaction with coordinator.

        @param retrieve_db: if this slave should retrieve entire data
        base from coordinator.
        """
	cmds_map = {
	    commands.UPDATE_DATA: self.do_update_data,
	    commands.QUIT: self.do_quit,
            commands.DUMP: self.do_dump,
	    }

	if retrieve_db:
	    self.send( commands.RETRIEVE_DB, None )

	self.keep_running = True
	while self.keep_running:
            try:
                self._debug( 4, "Waiting for command..." )
                cmd, data = self.recv()
            except socket.timeout, e:
                self._debug( 4, "Timeout: %s" % e )
                continue

            self._debug( 1, "Received command: %s, Data: %s" % ( cmd, data ) )
	    if not cmd:
                self._debug( 3, "No command, stop" )
		break

	    try:
		f = cmds_map[ cmd ]
		f( data )

                if self.introduced_delay is not None:
                    from random import randint
                    time.sleep( randint( 0, 1 ) * self.introduced_delay )

                if not self.recv_ack():
                    self._debug( 0, "Coordinator should send ACK!" )
                    return

                self.send_ack()

	    except KeyError, e:
                self._debug( 0, "Command '%s' doesn't exists!" % ( cmd, ) )

            except socket.timeout, e:
                self._debug( 0,
                             """\
------------------------------------------------------------
Socket operation timed out in %(timeout)s seconds.
Exception: %(exception)s
Traceback:

%(traceback)s

------------------------------------------------------------
""" % { 'timeout': self.timeout,
        'exception': e,
        'traceback': format_exc() } )
                break

            except socket.error, e:
                self._debug( 0, "Lost connection to server %s:%s: %s" % \
                             ( self.server_address + ( e, ) ) )
                break
            # end try
        # end while self.keep_running
    # run()


    def do_update_data( self, data ):
        """Execute action required by coordinator, updating self data.

        @param data: data to update.
        @todo: implement request id + response support.
        """
        req, je = data # ( requests info, db journal entries )

        for rid, version, value in req:
            self.__requests[ rid ]    = value
            self.__req_ver[ version ] = rid

        # replay journal entries from changed data.
        # TODO: we need to get the base version if we use shrink_journal!
        for l in je:
            for id, value in l:
                self.__db[ id ] = value
        # extend local journal with this one:
        self.__journal.extend( je )

        self._debug( 1, """\
----------------------------------------
Updated data: %(update_data)s
Data Base after update:
%(db)s

Journal Entries after update:
%(journal)s

Requests after update:
%(requests)s

Requests and their DB Version association:
%(req_ver)s,

----------------------------------------""" % \
                   { 'update_data': je,
                     'db': self.__db,
                     'journal': self.__journal,
                     'requests': self.__requests,
                     'req_ver': self.__req_ver,
                     } )
    # do_update_data()


    def do_quit( self, data ):
        """Execute action required by coordinator, quit running.

        @param data: ignored.
        """
        self._debug( 1, "Server want me to quit..." )
	self.keep_running = False
    # do_quit()


    def do_dump( self, data ):
        from bank import VersionedBank
        from db import VersionedDB

        fname = tempfile.mktemp( ".dump",
                                 "%s-" % os.path.basename( sys.argv[ 0 ] ) )
        f = open( fname, "w" )
        bank = VersionedBank( VersionedDB( self.__db, self.__journal ) )
        f.write( str( bank) )

        f.write( '\nRequests:\n' )
        reqs = self.__requests.items()
        reqs.sort( lambda a, b: cmp( a[ 0 ], b[ 0 ] ) )
        f.write( str( reqs ) )

        f.write( '\n\nRequests and associated versions:\n' )
        reqver = self.__req_ver.items()
        reqver.sort( lambda a, b: cmp( a[ 0 ], b[ 0 ] ) )
        f.write( str( reqver ) )
        f.write( '\n' )
        f.close()

        debug( 1, "Status dumped to file: %s" % fname )

        self.send( commands.ACK, fname )
    # do_dump()


    def _debug( self, level, message ):
        """Print message to standard error based on current debug level and
        message level.

        Message is only printed if C{level >= debug.level}.

        @param level: message level
        @type  level: L{int}
        @param message: what to print.
        """
        debug( level, "%s: %s" % ( self.__class__.__name__, message ) )
    # _debug()
# Slave

